<?php

class taglib_template {
	protected $source = "";
	protected $context = array();
	protected $cache;

	public $taglibs = array();
	public $loader;

	public function __toString() {
		return $this->parse();
	}

	/**
		* Fügt dem Context des Templates Daten hinzu, auf die zugegriffen werden kann
		* @param string $name Variablenname
		* @param mixed $data Wert
		* @return iv_template_abstract
		*/
	public function setContext( $name, $data ) {
		$this->context[$name] = $data;
		return $this;
	}

	/**
		* Läd Daten aus dem Context des Teampltes
		* @param string $name Name der Variable, welche ausgelesen werden soll
		* @return mixed Wert der abgefragten Variable
		*/
	public function getContext( $name ) {
		$pfad = explode( ".", $name );
		$cont = $this->context;
		foreach( $pfad as $e )
			if(is_array($cont)) $cont = $cont[$e];
			elseif(is_object($cont)) $cont = $cont->$e;
			else return NULL;
		return $cont;
	}

	/**
		* Überschreibt den Context mit dem angegebenen Array
		* @param array $context Der Context, welcher übertragen werden soll
		* @return iv_template_abstract
		*/
	public function cloneContext( &$context ) {
		foreach( $this->context as $key => $val )
			if( empty( $context[$key] ))
				$context[$key] = $val;

		$this->context = &$context;
		return $this;
	}

	/**
		* Interpretiert das Template und gibt es aus
		* @return iv_template_abstract
		*/
	public function display() {
		echo $this->parse();
		return $this;
	}

	/**
		* Erzeugt ein Template Objekt und erfordert den Quelltext
		* @param string $s Quellcode des Templates
		*/
	function  __construct( $s, $l ) {
		$this->source = $s;
		$this->loader = $l;
	}

	/**
		* Fügt eine Tagbibliothek hinzu
		* @param string $group Name über den die Lib aufgerufen werden kann
		* @param iv_template_taglib_abstract $lib Tagbibliothek
		* @return iv_template_taglib_template
		*/
	public function addLib( $group, taglib_abstract $lib ) {
		$lib->setTemplate( $this );
		$this->taglibs[$group] = $lib;
		return $this;
	}

	/**
		* Kopiert den eigenen Context in ein anderes Template
		* @param iv_template_abstract $temp Ziel Template
		* @return iv_template_taglib_template
		*/
	public function copyContext( taglib_template $temp ) {
		$temp->cloneContext( $this->context );
		return $this;
	}

	/**
		* Kopiert die eigenen Libs in ein anderes Template
		* @param iv_template_taglib_template $temp Ziel Template
		* @return iv_template_taglib_template
		*/
	public function copyLibs( taglib_template $temp ) {
		$temp->cloneLibs( $this->taglibs );
		return $this;
	}

	/**
		* Übernimmt die angegeben Libs
		* @param array $libs Bibliotheken
		* @return iv_template_taglib_template
		*/
	public function cloneLibs( $libs ) {
		foreach( $libs as $name => $lib )
			$this->addLib( $name, $lib );
		return $this;
	}

	/**
		* Interpretiert das Template und gibt das ergebnis zurück
		* @return string interpretiertes Template
		*/
	public function parse() {
		if( !$this->cache ) $this->cache = $this->getTags( $this->source );
		return $this->parse_area( 0, strlen( $this->source ), $this->cache );
	}

	/**
		* Interpretiert ein Tag
		* @param array $tag Zu interpretierendes Tag
		* @return string Interpretiertes Tag
		*/
	protected function parse_tag( $tag ) {
		preg_match_all( "/(?<name>\w+)=(\"|')(?<value>[}{\w\$\s\.]+)\\2/is", $tag['open']['source'], $reg_args, REG_OFFSET_CAPTURE | PREG_SET_ORDER );
		foreach( $reg_args as $arg ) $args[$arg['name']] = $arg['value'];
		return $this->taglibs[$tag['open']['group']]->doTag( $tag, $args );
	}

	/**
		* Interpretiert einen bestimmten Bereich
		* @param int $start Beginn des Bereichs
		* @param int $ende Ende des Bereichs
		* @param array $tags Tags die dieser Bereich enthält
		* @return string Interpretierter bereich
		*/
	public function parse_area( $start, $ende, $tags ) {
		$areasource = substr( $this->source, $start, $ende - $start );

		foreach( $tags as $tag )
			if( isset( $this->taglibs[$tag['open']['group']] ) && ($replace = $this->parse_tag( $tag )) !== false ) {
				$from = $tag['open']['position'] - $start;

				if( isset( $tag['close'] ))
					$to = $tag['close']['position'] - $start + strlen( $tag['close']['source'] );
				else
					$to = $tag['open']['position'] - $start + strlen( $tag['open']['source'] );


				$start -= strlen( $replace ) - $to + $from;
				$areasource = substr( $areasource, 0, $from ) . $replace . substr( $areasource, $to);
			}

		return $this->getExpression( $areasource );
	}

	/**
		* Fügt ein Tag in den Tagbaum ein
		* @param array $tagtree Baumartiges mehrdimensionales Array mit allen Tags
		* @param array $tag Einzufügendes Tag
		* @return int position oder so
		*/
	protected function insertTag( &$tagtree, $tag ) {
		foreach( $tagtree as $i => $ele )
			if( $ele['open']['position'] < $tag['open']['position'] && $ele['close']['position'] > $tag['open']['position'] )
				return $this->insertTag( $tagtree[$i]['tags'], $tag );

		$tagtree[] = $tag;
		return 1;
	}

	/**
		* Findet die im Template genutzten Tags und ordnet sie in einen Baum ein
		* @param string $s Quelltext des Templates
		* @return array Tagbaum
		*/
	protected function getTags( $s ) {
		$reg_opentags = array();
		$reg_closetags = array();
		$reg_standalonetags = array();
		$opentags = array();
		$closetags = array();
		$temptags = array();

		preg_match_all( "/<(?<group>\w+):(?<name>\w+)(\s+(\w+)=(\"|')([}{\w\$\s\.]+)\\5)*\s*>/is", $s, $reg_opentags, PREG_OFFSET_CAPTURE | PREG_SET_ORDER );
		preg_match_all( "/<\/(?<group>\w+):(?<name>\w+)>/is", $s, $reg_closetags, PREG_OFFSET_CAPTURE | PREG_SET_ORDER );
		preg_match_all( "/<(?<group>\w+):(?<name>\w+)(\s+(\w+)=(\"|')([}{\w\$\s\.]+)\\5)*\s*\/>/is", $s, $reg_standalonetags, PREG_OFFSET_CAPTURE | PREG_SET_ORDER );

		foreach( $reg_opentags as $tag )
			$opentags[] = array( "group" => $tag['group'][0], "name" => $tag['name'][0], "position" => $tag[0][1], "source" => $tag[0][0] );
		foreach( $reg_closetags as $tag )
			$closetags[] = array( "group" => $tag['group'][0], "name" => $tag['name'][0], "position" => $tag[0][1], "source" => $tag[0][0] );

		unset( $reg_opentags, $reg_closetags );

		foreach( $closetags as $tag ) {
			$open = -1;
			for( $i = 0; $i < count( $opentags ) && $opentags[$i]['position'] < $tag['position']; $i++ )
				if( $opentags[$i]['group'] == $tag['group'] && $opentags[$i]['name'] == $tag['name'] )
					$open = $i;

			if ( $open > -1 ) {
				$opentag = array_splice( $opentags, $open, 1 );
				$temptags[$opentag[0]['position']] = array( "open" => $opentag[0], "close" => $tag, "tags" => array() );
			}
		}

		foreach( $reg_standalonetags as $tag ) {
			$instag = array( "open" => array( "group" => $tag['group'][0], "name" => $tag['name'][0], "position" => $tag[0][1], "source" => $tag[0][0] ));
			$temptags[$tag[0][1]] = $instag;
		}

		ksort( $temptags );
		$tags = array();
		foreach( $temptags as $tag ) $this->insertTag( $tags, $tag );
		return $tags;
	}

	/**
		* Setzt den Cache für ein Template
		* @param mixed $cache Cachdaten
		* @return iv_template_abstract
		*/
	public function setCache( array $cache ) {
		$this->cache = $cache;
		return $this;
	}

	/**
		* Erzeugt einen Cache für das Template
		* @return array Cache
		*/
	public function getCache() {
		return $this->getTags( $this->source );
	}

	/**
		* Ersetzt in einem Quelltextausschnitt Variablen durch ihre Werte
		* @param string $source Quelltext mit Variablen
		* @return string Quelltext mit Werden
		*/
	public function getExpression( $source ) {
		return preg_replace_callback( "/\\\$\{([\w\.]+)\}/is", array($this, "expcallback"), $source );
	}

	/**
		* Callback von getExpression
		* @param array $treffer
		* @return mixed
		*/
	protected function expcallback( $treffer ) {
		return $this->getContext( $treffer[1] );
	}
}
